package com.hontech.bread.api

import android.os.Environment
import android.os.Handler
import com.hontech.bread.R
import com.hontech.bread.store.StoreAdvCfg
import com.hontech.bread.store.advDir
import com.hontech.bread.utils.AppTask
import com.hontech.bread.utils.ConnTask
import com.hontech.bread.utils.SERVER_WEB
import com.hontech.bread.utils.ServiceException
import com.hontech.bread.utils.Version
import com.hontech.bread.utils.log
import com.hontech.bread.utils.macAddr
import com.hontech.bread.utils.runAny
import com.hontech.bread.utils.runIO
import com.hontech.bread.utils.runNet
import com.hontech.bread.utils.runNewThread
import com.hontech.bread.utils.stringById
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.cbor.Cbor
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.concurrent.TimeUnit

const val HTTP_RESP_HEAD = "resp"
const val HTTP_RESP_OK = "ok"
const val HTTP_RESP_FAIL = "err"
const val HTTP_RESP_PANIC = "panic"

const val PUSH_REQ = "push_req"


const val HTTP_DEVICE_SESSION_HEAD = "device_token"
private var deviceId: Long? = null

private val client = OkHttpClient.Builder()
    .connectTimeout(10, TimeUnit.SECONDS)
    .readTimeout(20, TimeUnit.SECONDS)
    .writeTimeout(8, TimeUnit.SECONDS)
    .build()

private fun makeUrl(url: String) = "${SERVER_WEB}${url}"


@OptIn(ExperimentalSerializationApi::class)
private fun assertResp(resp: Response): Response {
    if (resp.code != 200) {
        throw IOException("http${stringById(R.string.t1733460848_0)}:${resp.message} ${resp.request.url}")
    }
    val respState = resp.headers[HTTP_RESP_HEAD] ?: throw IOException("no resp head")
    if (respState != HTTP_RESP_OK) {
        val body = resp.body?.bytes() ?: throw IOException("no body?")
        val info = Cbor.decodeFromByteArray<ErrInfo>(body)
        throw ServiceException(info)
    }
    return resp
}

@OptIn(ExperimentalSerializationApi::class)
private inline fun <reified T> parseResp(resp: Response): T {
    assertResp(resp)
    val buf = resp.body?.bytes() ?: throw IOException("no body?")
    return Cbor.decodeFromByteArray(buf)
}


private suspend inline fun <reified T> get(task: Handler, url: String): T {
    return runAny(task) {
        val req = Request.Builder().get()
            .url(makeUrl(url))
            .build()
        val resp = client.newCall(req).execute()
        parseResp<T>(resp)
    }
}

private suspend inline fun <reified T : Any> getWithToken(task: Handler, url: String): T {
    return runAny(task) {
        val req = Request.Builder().get()
            .url(makeUrl(url))
            .addHeader(HTTP_DEVICE_SESSION_HEAD, "$deviceId")
            .build()
        val resp = client.newCall(req).execute()
        parseResp<T>(resp)
    }
}

@OptIn(ExperimentalSerializationApi::class)
private suspend inline fun <reified T, reified R> post(task: Handler, url: String, req: T): R {
    return runAny(task) {
        val request = Request.Builder().post(Cbor.encodeToByteArray(req).toRequestBody())
            .url(makeUrl(url))
            .build()
        val resp = client.newCall(request).execute()
        parseResp<R>(resp)
    }
}

private suspend fun loginIfNot() {
    if (deviceId == null) {
        login()
    }
}

fun isLogin() = deviceId != null

suspend fun login() {
    val req = DeviceLoginReq(macAddr, Version)
    val id = post<DeviceLoginReq, Long>(ConnTask, "/api/device/login", req)
    deviceId = id
    log("${stringById(R.string.t1733460848_1)} ${stringById(R.string.t1733460848_2)}ID:${id}")
}

suspend fun poll() = runNet {
    val id = deviceId ?: throw IllegalStateException("${stringById(R.string.t1733460848_3)}")
    val request = Request.Builder().get()
        .url(makeUrl("/api/device/poll_message"))
        .addHeader(HTTP_DEVICE_SESSION_HEAD, "$id")
        .build()
    val resp = client.newCall(request).execute()
    assertResp(resp)
    val req = resp.headers[PUSH_REQ] ?: throw IllegalStateException("push req")
    val ls = req.split(",")
    val cmd = ls[0]
    val type = ls[1].toInt()
    val seq = ls[2].toInt()
    val body = resp.body?.bytes() ?: byteArrayOf()
    PushReq(cmd, type, seq, body)
}

suspend fun send(type: Int, seq: Int, ec: Int, body: ByteArray) = runIO {
    val id = deviceId ?: throw IllegalStateException("${stringById(R.string.t1733460848_4)}")
    val request = Request.Builder().post(body.toRequestBody())
        .url(makeUrl("/api/device/send_message?push_type=${type}&seq=${seq}&ec=${ec}"))
        .addHeader(HTTP_DEVICE_SESSION_HEAD, "$id")
        .build()
    val resp = client.newCall(request).execute()
    assertResp(resp)
}


const val CTYPE_APK = 0
const val CTYPE_BIN = 1

suspend fun firmQuery(cType: Int): Array<Firm> {
    return get(ConnTask, "/api/firm/query_all?firm_type=${cType}")
}

suspend fun downloadBin(id: Long): ByteArray {

    return runAny(ConnTask) {
        val req = Request.Builder().get()
            .url(makeUrl("/api/firm/download?id=${id}"))
            .build()
        val resp = client.newCall(req).execute()
        resp.body?.bytes() ?: throw IOException("no body")
    }
}

suspend fun advCfgQuery(): List<StoreAdvCfg> {
    loginIfNot()
    return getWithToken(ConnTask, "/api/adv_cfg/query_all")
}

fun downloadAdv(id: Long): String {
    log("${stringById(R.string.t1733460848_5)}:$id")
    val file = File(advDir, "$id")
    val req = Request.Builder()
        .url(makeUrl("/api/adv/download?id=$id"))
        .get()
        .build()
    val resp = client.newCall(req).execute()
    val out = FileOutputStream(file)
    resp.use {
        out.use {
            val body = resp.body ?: throw IOException("no body")
            val input = body.byteStream()
            val size = body.contentLength()
            val buf = ByteArray(10 * 1024)
            var index = 0
            while (index < size) {
                val n = input.read(buf)
                if (n <= 0) {
                    throw IOException("download fail")
                }
                out.write(buf, 0, n)
                index += n
            }
        }
    }
    return file.path
}

suspend fun downloadApk(id: Long, fileName: String, cb: (prog: Int) -> Unit): String {

    return runNewThread {
        val req = Request.Builder()
            .url(makeUrl("/api/firm/download?id=${id}"))
            .get()
            .build()

        val resp = client.newCall(req).execute()

        val file = File(Environment.getExternalStorageDirectory(), fileName)
        val out = FileOutputStream(file)
        resp.use {
            out.use {
                val body = resp.body ?: throw IOException("no body?")
                val input = body.byteStream()
                val size = body.contentLength()
                val buf = ByteArray(10 * 1024)
                var index = 0
                while (index < size) {
                    val n = input.read(buf)
                    if (n <= 0) {
                        throw IOException("download fail")
                    }
                    out.write(buf, 0, n)
                    index += n
                    AppTask.post { cb((index * 100 / size).toInt()) }
                }
            }
        }
        file.path
    }
}

